<?php

namespace system\core;

class selector
{

	private $selector;
	private $level;
	private $lookupFilter;
	private $matchTargetType;
	private $remainingSelector;
	private $parentObject;
	private $subSelector;
	private $nextRule;
	private static $definedConstants;

	/**
	 * Constructor. Initializes some used constants and assigns the given object as parent to the new created object. Then it starts analyzing the given selector statement.
	 * @param type $selector
	 * @param type $parentObject
	 */
	function __construct($selector, &$parentObject=null)
	{
		self::defineConstants();
		$selector = self::removeGlitches($selector);
		if( $parentObject ) $this->setParent($parentObject);

		if( !$parentObject && strpos($selector, ",") === false )
		{
			$this->setLevel(1);
		}
		elseif( $parentObject )
		{
			$this->setLevel($parentObject->getLevel() + 1);
		}

		$this->parseSelector($selector);
	}

	/**
	 * Definition of constants used within this class
	 * @return void
	 */
	private static function defineConstants()
	{
		if( self::$definedConstants ) return;
		define("OP_NEXT", " ");
		define("OP_FOLLOWEDBY", ">");
		define("OP_DIRECTSIBLING", "+");
		define("OP_SIBLING", "~");
		define("OP_CLASS", ".");
		define("OP_ID", "#");
		define("OP_PSEUDO", ":");
		define("OP_ATTRIBUTE", "[");
		define("OP_SUBSELECT", "(");
		define("PARENTS", "<");
		define("PARENT", "<");
		define("CHILDREN", ">");
		define("CHILD", ">");
		define("SIBLINGS", "=");
		define("SIBLING", "=");
		define("ELEMENT", "0");
		define("LOOK_AT_NEXT", "1");
		define("LOOK_AT_ALL", "*");
		define("LOOK_AT_THIS", "=");

		self::$definedConstants = true;
	}

	public function readout()
	{
		if( !$this->getLevel() )
		{
			$lex = "Der String besteht aus";
			if( $this->getTargetType() == "MULTI" )
			{
				$lex = "Der String besteht aus";
				$c = count($this->getNextRule());
				$lex.= " $c Selektoren.\n";
			}
			else
			{
				$lex.= " einem Selektor.\n";
			}
		}
		else
		{
			if( $this->getTargetType() == "SUBSELECT" )
			{
				foreach( $this->getSubSelector() as $sub )
				{
					$lex.= $sub->readout();
				}
			}
			switch ($this->getLookupFilter())
			{
				case "=0":
					$lex.= "Das Element hat auch";
					break;
				case "*>":
					if( $this->getLevel() == 1 )
					{
						$lex.= "Suche";
					}
					else
					{
						$lex.= "Darunter";
					}
					break;
				case "1>":
					$lex.= "Eine Ebene tiefer";
					break;
				case "1=":
					$lex.= "In gleicher Ebene direkt gefolgt von";
					break;
				case "*=":
					$lex.= "In gleicher Ebene irgendwo";
					break;
			}
			switch ($this->getTargetType())
			{
				case "TAG":
					$lex.= " einen <".$this->getSelector().">-Tag";
					break;
				case "CLASS":
					$lex.= " die Klasse ".implode(" und ", explode(".", $this->getSelector()));
					break;
				case "ATTRIBUTE":
					$lex.= " ein Attribut ".$this->getSelector();
					break;
				case "PSEUDO":
					$lex.= " einen Pseudoselector ".$this->getSelector();
					break;
			}
			switch ($this->getLookupFilter())
			{
				case "*>":
					if( $this->getLevel() == 1 )
					{
						$lex.= " im gesamten Dokument.\n";
					}
					else
					{
						$lex.= " in dem gefundenen Zweig.\n";
					}
					break;
				case "=0":
					$lex.= ".\n";
					break;
				case "1>":
					$lex.= " der direkt auf das vorangegangene Element folgt.\n";
					break;
				case "1=":
				case "*=":
					$lex.= ".\n";
					break;
			}
		}
		if( $this->getNextRule() ) foreach( $this->getNextRule() as $nr => $obj )
			{
				if( !$this->getLevel() )
				{
					$lex.= "Selector ".($nr + 1)." \"".$obj->getSelector().$obj->getRemaining()."\"\n";
				}
				$lex.= $obj->readout();
			}
		//print_r($this);
		return $lex;
	}

	/**
	 * The igniter. This static function calls the internalParser
	 * @param type $string
	 * @return thisClass
	 */
	public static function parser($string)
	{
		return self::internalParser($string);
	}

	/**
	 * Function validates the given parameter and creates a new object of it self.
	 * @param type $string
	 * @param type $parentObject
	 * @return thisClass
	 */
	private static function internalParser($string, &$parentObject=null)
	{
		if( !trim($string) ) return false;
		$thisClass = get_called_class();
		$selectorObject = new $thisClass($string, $parentObject); //== setSelector($string)
		return $selectorObject;
	}

	/**
	 * Function to clean up senseless formatting character within a selector
	 * @param type $string
	 * @return string
	 */
	private static function removeGlitches($string)
	{
		$string = preg_replace('!\s*([>+~])\s*!', "\$1", $string);
		return $string;
	}

	/**
	 * Pre-check a given selector statement to check if it consists of more than one statement delimited by (,)
	 * @param type $string
	 * @return boolean
	 */
	private function checkMultiSelector($string)
	{
		if( strpos($string, ",") !== false )
		{
			$selectorArray = preg_split("!\s*,\s*!sm", $string);
			if( $selectorArray )
			{
				$this->setTargetType("MULTI");
				$this->setLookupFilter("*");
				foreach( $selectorArray as $selector )
				{
					$nextrule = self::internalParser(" ".$selector, $this);
					if( $nextrule )
					{
						$this->nextRule($nextrule);
					}
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * If given string is only one statement, start parsing it
	 * @param type $string
	 */
	private function checkSingleSelector($string)
	{
//		echo $string."\n";
		$selectorInfo = $this->getSelectorType($string);
		$this->setRemaining($selectorInfo["nextrule"]);
//		print_r($selectorInfo);
		if( !$selectorInfo ) return false;

		$this->setSelector($selectorInfo["thisselector"]);
		$this->setLookupFilter($selectorInfo["lookupFilter"]);

		$this->setTargetType($selectorInfo["thistyp"]);
		if( $selectorInfo["nextrule"] )
		{
			$nextrule = self::internalParser($selectorInfo["nextrule"], $this);
			if( $nextrule )
			{
				$this->nextRule($nextrule);
			}
		}
	}

	/**
	 * Validate the given string and determine wheter it is a simple single statement or an multi statement. Otherwise do nothing else than return.
	 * @param type $string
	 */
	private function parseSelector($string)
	{
		if( !trim($string) || !is_string($string) ) return false;

		$this->setSelector($string);

		if( !$this->checkMultiselector($string) )
		{
			$this->checkSingleSelector($string);
		}
	}

	private function nextRule($selectorObject)
	{
		$this->nextRuleArray[] = $selectorObject;
	}

	private function subSelector($selectorObject)
	{
		$this->subSelector[] = $selectorObject;
	}

	private function setRemaining($selector)
	{
		$this->remainingSelector = $selector;
	}

	public function getRemaining()
	{
		return $this->remainingSelector;
	}

	private function setParent(&$object)
	{
		$this->parentObject = & $object;
	}

	private function getParent()
	{
		return $this->parentObject;
	}

	public function getSelector()
	{
		return $this->selector;
	}

	private function getSubSelector()
	{
		return $this->subSelector;
	}

	private function setSelector($string)
	{
		$this->selector = $string;
	}

	private function setTargetType($string)
	{
		$this->matchTargetType = $string;
	}

	public function getTargetType()
	{
		return $this->matchTargetType;
	}

	private function setLookupFilter($string)
	{
		$this->lookupFilter = $string;
	}

	public function getLookupFilter()
	{
		return $this->lookupFilter;
	}

	public function getLookupLevel()
	{
		return $this->lookupFilter[0];
	}

	public function getLookupDirection()
	{
		return $this->lookupFilter[1];
	}

	private function setLevel($level)
	{
		$this->level = $level;
	}

	public function getLevel()
	{
		return $this->level;
	}

	public function getNextRule()
	{
		return $this->nextRuleArray;
	}

	public function hasNextRule()
	{
		if( $this->nextRuleArray ) return true;
	}

	public static function readLookupFilter($code)
	{
		return $code." means ".self::translateLookupFilter($code);
	}

	public static function translateLookupFilter($code)
	{
		$k[0]["1"] = "LOOK_AT_NEXT";
		$k[0]["*"] = "LOOK_AT_ALL";
		$k[0]["="] = "LOOK_AT_THIS";
		$k[1]["<"] = "PARENTS";
		$k[1][">"] = "CHILDREN";
		$k[1]["="] = "SIBLINGS";
		$k[1]["0"] = "ELEMENT";
		return $k[0][$code[0]].".".$k[1][$code[1]];
	}

	private static function progressOne($string)
	{
		return substr($string, 1);
	}

	private function getSelectorType($string, $precastedLookup=null)
	{
//		echo "TRACE: (".$string.")\n";

		if( !trim($string) ) return;
		$operator = substr($string, 0, 1);

//		echo "OPERATOR: (".$operator.")\n";
//		if($precastedLookup)
//			echo "precastedLookup: ".$precastedLookup."\n";
//		echo "Test Operator: '{$operator}'\n";
		//TODO more convenient operator parser
		switch ($operator)
		{
			case OP_NEXT:
//				echo "NEXT\n";
				$return = $this->getSelectorType(self::progressOne($string), LOOK_AT_ALL.CHILDREN);
				break;
			case OP_FOLLOWEDBY:
//				echo "FOLLOWEDBY\n";
				$return = $this->getSelectorType(self::progressOne($string), LOOK_AT_NEXT.CHILDREN);
				break;
			case OP_SIBLING:
//				echo "SIBLING\n";
				$return = $this->getSelectorType(self::progressOne($string), LOOK_AT_ALL.SIBLING);
				break;
			case OP_DIRECTSIBLING:
//				echo "DIRECTSIBLING\n";
				$return = $this->getSelectorType(self::progressOne($string), LOOK_AT_NEXT.SIBLING);
				break;
			case OP_CLASS:
//				echo "CLASS\n";
				$return = $this->traceCLASS($string);
				$return["lookupFilter"] = LOOK_AT_THIS.ELEMENT;
				break;
			case OP_ID:
//				echo "ID\n";
				$return = $this->traceID($string);
				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_THIS.ELEMENT;
				break;
//			case "~":
//				$return = $this->traceSIBLING($string);
//				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_ALL.SIBLING;
//				break;
//			case "+":
//				$return = $this->traceDIRECTSIBLING($string);
//				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_NEXT.SIBLING;
//				break;
			case OP_PSEUDO:
//				echo "PSEUDO\n";
				$return = $this->tracePSEUDO($string);
				if( !$this->getParent() ) $precastedLookup = LOOK_AT_ALL.CHILDREN;
				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_THIS.ELEMENT;
				break;
			case OP_ATTRIBUTE:
//				echo "ATTRIBUTE\n";
				$return = $this->traceATTRIBUTE($string);
				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_THIS.ELEMENT;
				break;
			case OP_SUBSELECT:
//				echo "SUBSELECT";
				$return = $this->traceSUBSELECT($string);
				$this->subSelector(self::internalParser($return["thisselector"]));
				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_THIS.ELEMENT;
				break;
			default:
//				echo "TAG\n";
				$return = $this->traceTAG($string);
				if( !$precastedLookup ) $return["lookupFilter"] = LOOK_AT_ALL.CHILDREN;
				break;
		}
		if( $precastedLookup ) $return["lookupFilter"] = $precastedLookup;
		return $return;
	}

	private function traceResult($typ, $match)
	{
		$return = array("thistyp" => $typ,
				"thisselector" => $match["thisselector"],
				"nextrule" => $match["nextrule"]);
//		print_r($return);
		return $return;
	}

	private function traceTAG($string)
	{
		if( preg_match('/(?<thisselector>^[a-z]+[a-z0-9]*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("TAG", $match);
		}
		return false;
	}

	private function traceCLASS($string)
	{
		if( preg_match('/\.(?<thisselector>(?:[a-z.]+[a-z0-9]*)*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("CLASS", $match);
		}
		return false;
	}

	private function traceID($string)
	{
		if( preg_match('/\#(?<thisselector>[a-z]+[a-z0-9]*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("ID", $match);
		}
		return false;
	}

	private function traceDIRECTSIBLING($string)
	{
		if( preg_match('/\+(?<thisselector>[a-z]+[a-z0-9]*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("DIRECTSIBLING", $match);
		}
		return false;
	}

	private function traceSIBLING($string)
	{
		if( preg_match('/\~(?<thisselector>[a-z]+[a-z0-9]*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("SIBLING", $match);
		}
		return false;
	}

	private function tracePSEUDO($string)
	{
		if( preg_match('/\:(?<thisselector>[a-z]+[a-z0-9]*)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("PSEUDO", $match);
		}
		return false;
	}

	private function traceATTRIBUTE($string)
	{
		if( preg_match('/\[(?<thisselector>[^\]]*)\](?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("ATTRIBUTE", $match);
		}
		return false;
	}

	private function traceSUBSELECT($string)
	{
		if( preg_match('/\((?<thisselector>[^\)]*)\)(?<nextrule>.*)/ism', $string, $match) )
		{
			return $this->traceResult("SUBSELECT", $match);
		}
		return false;
	}

}

?>
